# frozen_string_literal: true

require 'spec_helper'

RSpec.describe API::VulnerabilityExports, feature_category: :vulnerability_management do
  include AccessMatchersForRequest

  before do
    stub_licensed_features(security_dashboard: true)
  end

  let_it_be(:user) { create(:user) }
  let_it_be(:project) { create(:project, :with_vulnerability) }

  shared_examples 'creating export for exportable' do
    let(:format) { 'csv' }
    let(:deny_setup) {}
    let(:permission_setup) {}

    subject(:create_vulnerability_export) { post api(request_path, user), params: { export_format: format } }

    context 'when the request does not fulfill the requirements' do
      let(:format) { 'exif' }

      it 'responds with bad_request', :aggregate_failures do
        create_vulnerability_export

        expect(response).to have_gitlab_http_status(:bad_request)
        expect(json_response).to eq('error' => 'export_format does not have a valid value')
      end
    end

    context 'when the request fulfills the requirements' do
      context 'when the user is not authorized to take the action' do
        before do
          deny_setup
        end

        it 'responds with 403 forbidden' do
          create_vulnerability_export

          expect(response).to have_gitlab_http_status(:forbidden)
        end
      end

      context 'when the user is authorized to take the action' do
        let(:mock_service_object) { instance_double(VulnerabilityExports::CreateService, execute: vulnerability_export) }

        before do
          allow(VulnerabilityExports::CreateService).to receive(:new).and_return(mock_service_object)
          permission_setup
        end

        context 'when the export creation succeeds' do
          let(:vulnerability_export) { create(:vulnerability_export) }

          it 'returns information about new vulnerability export', :aggregate_failures do
            create_vulnerability_export

            expect(response).to have_gitlab_http_status(:created)
            expect(response).to match_response_schema('public_api/v4/vulnerability_export', dir: 'ee')
          end
        end

        context 'when the export creation fails' do
          let(:errors) { instance_double(ActiveModel::Errors, any?: true, messages: ['foo']) }
          let(:vulnerability_export) { instance_double(Vulnerabilities::Export, persisted?: false, errors: errors) }

          it 'returns the error message', :aggregate_failures do
            create_vulnerability_export

            expect(response).to have_gitlab_http_status(:bad_request)
            expect(json_response).to eq('message' => ['foo'])
          end
        end
      end

      it_behaves_like 'forbids access to vulnerability API endpoint in case of disabled features' do
        before do
          permission_setup
        end
      end
    end
  end

  describe 'POST /security/projects/:id/vulnerability_exports' do
    it_behaves_like 'creating export for exportable' do
      let(:request_path) { "/security/projects/#{project.id}/vulnerability_exports" }
      let(:deny_setup) { project.add_guest(user) }
      let(:permission_setup) { project.add_developer(user) }

      describe 'permissions', :enable_admin_mode do
        it { expect { create_vulnerability_export }.to be_allowed_for(:admin) }
        it { expect { create_vulnerability_export }.to be_allowed_for(:owner).of(project) }
        it { expect { create_vulnerability_export }.to be_allowed_for(:maintainer).of(project) }
        it { expect { create_vulnerability_export }.to be_allowed_for(:developer).of(project) }
        it { expect { create_vulnerability_export }.to be_allowed_for(:auditor) }

        it { expect { create_vulnerability_export }.to be_denied_for(:reporter).of(project) }
        it { expect { create_vulnerability_export }.to be_denied_for(:guest).of(project) }
        it { expect { create_vulnerability_export }.to be_denied_for(:anonymous) }
      end
    end
  end

  describe 'POST /security/groups/:id/vulnerability_exports' do
    let_it_be(:group) { create(:group) }

    it_behaves_like 'creating export for exportable' do
      let(:request_path) { "/security/groups/#{group.id}/vulnerability_exports" }
      let(:permission_setup) { group.add_developer(user) }
    end
  end

  describe 'POST /security/vulnerability_exports' do
    it_behaves_like 'creating export for exportable' do
      let(:request_path) { "/security/vulnerability_exports" }
      let(:deny_setup) { allow(InstanceSecurityDashboard).to receive(:new).and_return(nil) }
    end
  end

  describe 'GET /security/vulnerability_exports/:id' do
    let_it_be(:vulnerability_export) { create(:vulnerability_export, :finished, :csv, :with_csv_file, project: project, author: user) }

    let(:request_path) { "/security/vulnerability_exports/#{vulnerability_export.id}" }

    subject(:get_vulnerability_export) { get api(request_path, user) }

    context 'with an authorized user with proper permissions' do
      before do
        project.add_developer(user)
      end

      context 'when export is finished' do
        it 'returns information about vulnerability export', :aggregate_failures do
          get_vulnerability_export

          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to match_response_schema('public_api/v4/vulnerability_export', dir: 'ee')
          expect(json_response['id']).to eq vulnerability_export.id
        end

        it 'does not return Poll-Interval header' do
          get_vulnerability_export

          expect(response.headers['Poll-Interval']).to be_blank
        end
      end

      context 'when export is running' do
        let_it_be(:vulnerability_export) { create(:vulnerability_export, :running, :csv, project: project, author: user) }

        it 'returns information about vulnerability export', :aggregate_failures do
          get_vulnerability_export

          expect(response).to have_gitlab_http_status(:accepted)
          expect(response).to match_response_schema('public_api/v4/vulnerability_export', dir: 'ee')
          expect(json_response['id']).to eq vulnerability_export.id
        end

        it 'returns Poll-Interval header with value set to 5 seconds' do
          get_vulnerability_export

          expect(response.headers['Poll-Interval']).to eq '5000'
        end
      end
    end

    describe 'permissions', :enable_admin_mode do
      context 'for export author' do
        before do
          project.add_developer(user)
        end

        it { expect { get_vulnerability_export }.to be_allowed_for(user) }
      end

      it { expect { get_vulnerability_export }.to be_denied_for(:admin) }
      it { expect { get_vulnerability_export }.to be_denied_for(:owner).of(project) }
      it { expect { get_vulnerability_export }.to be_denied_for(:maintainer).of(project) }
      it { expect { get_vulnerability_export }.to be_denied_for(:developer).of(project) }
      it { expect { get_vulnerability_export }.to be_denied_for(:auditor) }
      it { expect { get_vulnerability_export }.to be_denied_for(:reporter).of(project) }
      it { expect { get_vulnerability_export }.to be_denied_for(:guest).of(project) }
      it { expect { get_vulnerability_export }.to be_denied_for(:anonymous) }
    end
  end

  describe 'GET /security/vulnerability_exports/:id/download' do
    let!(:vulnerability_export) { create(:vulnerability_export, :finished, :csv, :with_csv_file, project: project, author: user) }
    let(:request_path) { "/security/vulnerability_exports/#{vulnerability_export.id}/download" }

    subject(:download_vulnerability_export) { get api(request_path, user) }

    context 'with an authorized user with proper permissions' do
      before do
        project.add_developer(user)
      end

      context 'when export is running' do
        let!(:vulnerability_export) { create(:vulnerability_export, :running, :csv, project: project, author: user) }

        it 'renders 404', :aggregate_failures do
          download_vulnerability_export

          expect(response).to have_gitlab_http_status(:not_found)
          expect(json_response).to eq('message' => '404 Vulnerability Export Not Found')
        end
      end

      context 'when export is failed' do
        let!(:vulnerability_export) { create(:vulnerability_export, :failed, :csv, project: project, author: user) }

        it 'renders 404' do
          download_vulnerability_export

          expect(response).to have_gitlab_http_status(:not_found)
          expect(json_response).to eq('message' => '404 Vulnerability Export Not Found')
        end
      end

      context 'when export is finished' do
        it 'renders 200 with CSV file', :aggregate_failures do
          download_vulnerability_export

          expect(response).to have_gitlab_http_status(:ok)
          expect(response.body).to include 'Scanner Type,Scanner Name,Status,Vulnerability,Details,Additional Info,Severity,CVE'
          expect(response.headers['Poll-Interval']).to be_blank
        end
      end
    end

    describe 'permissions' do
      context 'for export author', :enable_admin_mode do
        before do
          project.add_developer(user)
        end

        it { expect { download_vulnerability_export }.to be_allowed_for(user) }
      end

      it { expect { download_vulnerability_export }.to be_denied_for(:admin) }
      it { expect { download_vulnerability_export }.to be_denied_for(:owner).of(project) }
      it { expect { download_vulnerability_export }.to be_denied_for(:maintainer).of(project) }
      it { expect { download_vulnerability_export }.to be_denied_for(:developer).of(project) }
      it { expect { download_vulnerability_export }.to be_denied_for(:auditor) }
      it { expect { download_vulnerability_export }.to be_denied_for(:reporter).of(project) }
      it { expect { download_vulnerability_export }.to be_denied_for(:guest).of(project) }
      it { expect { download_vulnerability_export }.to be_denied_for(:anonymous) }
    end
  end
end
